/*
 *    Copyright (C) 2011-2012 Hai Bison
 *
 *    See the file LICENSE at the root directory of this project for copying
 *    permission.
 */

/*
 * DialogUpdater.java
 *
 * Created on Sep 24, 2011, 4:06:03 PM
 */
package group.pals.mailgate;

import group.pals.mailgate.beans.FileTypes;
import group.pals.mailgate.preferences.Preferences;
import group.pals.mailgate.threads.BinImageFileDecoder;
import group.pals.mailgate.ui.RightAlignCellRenderer;
import group.pals.mailgate.utils.Utils;
import group.pals.threads.BaseThread;
import group.pals.threads.ThreadManager;
import group.pals.threads.listeners.ThreadAdapter;
import group.pals.threads.listeners.ThreadManagerAdapter;
import group.pals.utils.math.Converter;
import group.pals.utils.sys.MessageBox;
import group.pals.utils.updater.listeners.UpdateHandlerAdapter;
import group.pals.utils.updater.UpdateHandler;
import group.pals.utils.updater.beans.InfoFile;
import group.pals.utils.updater.beans.ItemUpdate;
import group.pals.utils.updater.exceptions.BadFormatException;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.SwingConstants;
import javax.swing.WindowConstants;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;

/**
 *
 * @author Hai Bison
 */
public class DialogUpdater extends javax.swing.JDialog {

    public static final boolean Debug = false;
    private final javax.swing.JDialog Instance = this;
    private final List<ItemUpdate> ItemUpdates;
    private final DefaultTableModel TableItemUpdatesModel;
    private final Map<String, Integer> MapColsTableItemUpdates;
    private final int ColItemUpdateIdx;
    private final int ColStatusIdx;
    private final Utils _Utils;
    private final Preferences _Preferences;
    private final ThreadManager _ThreadManager;
    private UpdateHandler updateHandler = null;
    /**
     * Total errors.
     */
    private int errorCount = 0;

    /** Creates new form DialogUpdater */
    public DialogUpdater(java.awt.Frame parent, boolean modal, InfoFile infoFile,
            List<ItemUpdate> itemUpdates) {
        super(parent, modal);
        initComponents();

        this.ItemUpdates = itemUpdates;
        this._Utils = Utils.getInstance();
        this._Preferences = Preferences.getInstance();
        /*
         * NOTE: always get new instance, because current instance is being used
         * in global application.
         */
        this._ThreadManager = ThreadManager.getNewInstance();

        initTableItemUpdates();
        this.TableItemUpdatesModel = (DefaultTableModel) tableItemUpdates.getModel();
        this.MapColsTableItemUpdates = new HashMap<String, Integer>();
        for (String header : TableItemUpdatesHeaders) {
            this.MapColsTableItemUpdates.put(header, tableItemUpdates.getColumn(header).getModelIndex());
        }
        ColItemUpdateIdx = MapColsTableItemUpdates.get(ColItemUpdate);
        ColStatusIdx = MapColsTableItemUpdates.get(ColStatus);

        initTableData();
        initActions();
        initListeners();

        initUI(parent);
    }

    /*----------------------------------------------------------------------------
     * Table ItemUpdates
     */
    private final String ColRowNo = "#";
    private final String ColItemUpdate = "Module";
    private final String ColStatus = "Status";
    private final String[] TableItemUpdatesHeaders = {ColRowNo, ColItemUpdate, ColStatus};
    private final Class[] TableItemUpdatesHeaderClasses = {String.class,
        ItemUpdate.class, String.class};

    //<editor-fold defaultstate="collapsed" desc="table initialization">
    private void initTableItemUpdates() {
        tableItemUpdates.setModel(new javax.swing.table.DefaultTableModel(
                new Object[][]{}, TableItemUpdatesHeaders) {

            @Override
            public Class getColumnClass(int columnIndex) {
                return TableItemUpdatesHeaderClasses[columnIndex];
            }

            @Override
            public boolean isCellEditable(int rowIndex, int columnIndex) {
                return false;
            }

            @Override
            public Object getValueAt(int row, int column) {
                final String ColName = getColumnName(column);

                if (ColName.equals(ColRowNo)) {
                    return String.format("%,02d", tableItemUpdates.convertRowIndexToView(row) + 1);
                } else if (ColName.equals(ColItemUpdate)) {
                    return super.getValueAt(row, column);
                } else {
//          ItemUpdate emailReader = (ItemUpdate)
//                  super.getValueAt(row, MapColsTableItemUpdates.get(ColItemUpdate));
                    return super.getValueAt(row, column);
                }
            }//getValueAt()
        });

        //------------------------ Init columns
        RightAlignCellRenderer rightAlignCellRenderer = new RightAlignCellRenderer();
        rightAlignCellRenderer.setCellHorzAlignment(SwingConstants.RIGHT);

        TableColumn col = tableItemUpdates.getColumn(ColRowNo);
        col.setCellRenderer(rightAlignCellRenderer);
        col.setMinWidth(50);
        col.setMaxWidth(50);
    }// </editor-fold>

    private void initTableData() {
        final int Col = MapColsTableItemUpdates.get(ColItemUpdate);
        Object[] row = new Object[TableItemUpdatesHeaders.length];
        for (ItemUpdate item : ItemUpdates) {
            row[Col] = item;
            TableItemUpdatesModel.addRow(row);
        }
        TableItemUpdatesModel.fireTableDataChanged();
    }

    private void initActions() {
        actionRestartUpdate = new ActionRestartUpdate();
        buttonRestart.setAction(actionRestartUpdate);
        actionClose = new ActionClose();
        buttonClose.setAction(actionClose);
    }

    private void initListeners() {
        _ThreadManager.addThreadManagerListener(new ThreadManagerAdapter() {

            @Override
            public synchronized void listChanged(ThreadManager sender, int action) {
                if (action < 0 && sender.isEmpty() && updateHandler == null) {
                    showFinishMessage();
                    actionRestartUpdate.setEnabled(true);
                }
            }
        });

        addWindowListener(new WindowAdapter() {

            @Override
            public void windowClosing(WindowEvent e) {
                if ((_ThreadManager.isEmpty() && updateHandler == null)
                        || MessageBox.confirmYesNo(Instance, null, "The update is in progress.\nAre you sure you want to cancel?", 1)) {
                    _ThreadManager.interruptAll();
                    setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
                } else {
                    setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
                }
            }
        });
    }

    private void initUI(java.awt.Frame parent) {
        setTitle("Updater");
        final int width = 600;
        setSize(width, width / 16 * 9);//wide screen  :-)
        setResizable(true);
        setLocationRelativeTo(parent);
    }

    /**
     * Starts updating, should be call only once.
     */
    public void start() {
        if (updateHandler != null) {
            return;
        }

        //reset error-count
        errorCount = 0;

        updateHandler = new UpdateHandler(ItemUpdates, _Utils.getStartupDir().getAbsolutePath());
        updateHandler.setUserAgent(
                "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.864.0 Safari/535.2");

        updateHandler.addUpdateHandlerListener(new UpdateHandlerAdapter() {

            @Override
            public synchronized void progressReport(UpdateHandler sender, ItemUpdate itemUpdate, long downloadedSize) {
                for (int row = 0; row < TableItemUpdatesModel.getRowCount(); row++) {
                    if (itemUpdate.equals(TableItemUpdatesModel.getValueAt(row, ColItemUpdateIdx))) {
                        TableItemUpdatesModel.setValueAt(String.format("Downloading %s %s...",
                                Converter.sizeToStr(downloadedSize),
                                itemUpdate.getDownloadFilesize() <= 0 ? ""
                                : String.format("(%s)", Converter.percentToStr(100.0 * downloadedSize / itemUpdate.getDownloadFilesize()))),
                                row, ColStatusIdx);

                        TableItemUpdatesModel.fireTableDataChanged();
                        break;
                    }//if
                }//for
            }
        });

        updateHandler.addThreadListener(new ThreadAdapter() {

            @Override
            public synchronized void threadRaised(BaseThread sender, Exception ex, Object obj) {
                errorCount++;
                for (int row = 0; row < TableItemUpdatesModel.getRowCount(); row++) {
                    if (obj.equals(TableItemUpdatesModel.getValueAt(row, ColItemUpdateIdx))) {
                        TableItemUpdatesModel.setValueAt(String.format("Exception: %s", ex), row, ColStatusIdx);
                        TableItemUpdatesModel.fireTableDataChanged();
                        break;
                    }//if
                }//for
            }

            @Override
            public synchronized void threadStarted(BaseThread sender) {
                _Utils.stopUpdater();
                actionRestartUpdate.setEnabled(false);

                new BaseThread(null) {

                    @Override
                    public void run() {
                        if (!isVisible()) {
                            setVisible(true);
                        }
                    }
                }.start();
            }

            @Override
            public synchronized void threadSignaled(BaseThread sender, Object message, Object obj) {
                switch ((Integer) message) {
                    case UpdateHandler.KeyDownloadDone: {
                        Map<ItemUpdate, File> info = (Map<ItemUpdate, File>) obj;
                        for (int row = 0; row < TableItemUpdatesModel.getRowCount(); row++) {
                            ItemUpdate itemUpdate = (ItemUpdate) info.keySet().toArray()[0];
                            if (itemUpdate.equals(TableItemUpdatesModel.getValueAt(row, ColItemUpdateIdx))) {
                                if (FileTypes.FileBinaryImage.equals(itemUpdate.getDownloadFiletype())) {
                                    TableItemUpdatesModel.setValueAt("Decoding...", row, ColStatusIdx);
                                    createBinImageFileDecoder(info.get(itemUpdate), itemUpdate);
                                } else {
                                    /*
                                     * check for launcher or launcher-loader update,
                                     * then store ItemUpdate to file
                                     */
                                    try {
                                        checkForUpdatesOfLaunchers(info.get(itemUpdate), itemUpdate);
                                        storeNewItemUpdate(itemUpdate);
                                        TableItemUpdatesModel.setValueAt("Done", row, ColStatusIdx);
                                    } catch (Exception ex) {
                                        errorCount++;
                                        TableItemUpdatesModel.setValueAt(String.format("Exception: %s", ex), row, ColStatusIdx);
                                    }
                                }

                                TableItemUpdatesModel.fireTableDataChanged();
                                break;
                            }//if
                        }//for
                        break;
                    }//KeyDownloadDone

                    case UpdateHandler.KeyDeleteDone: {
                        Map<ItemUpdate, Boolean> info = (Map<ItemUpdate, Boolean>) obj;
                        for (int row = 0; row < TableItemUpdatesModel.getRowCount(); row++) {
                            ItemUpdate itemUpdate = (ItemUpdate) info.keySet().toArray()[0];
                            if (itemUpdate.equals(TableItemUpdatesModel.getValueAt(row, ColItemUpdateIdx))) {
                                if (info.get(itemUpdate)) {
                                    TableItemUpdatesModel.setValueAt("Deleted...", row, ColStatusIdx);
                                    try {
                                        storeNewItemUpdate(itemUpdate);
                                    } catch (Exception ex) {
                                        errorCount++;
                                        TableItemUpdatesModel.setValueAt(String.format("Exception: %s", ex), row, ColStatusIdx);
                                    }
                                } else {
                                    errorCount++;
                                    TableItemUpdatesModel.setValueAt(
                                            String.format("Can not delete: \"%s/%s\"", itemUpdate.getFilepath(), itemUpdate.getFilename()),
                                            row, ColStatusIdx);
                                }

                                TableItemUpdatesModel.fireTableDataChanged();
                                break;
                            }//if
                        }//for
                        break;
                    }//KeyDeleteDone
                }//switch
            }

            @Override
            public synchronized void threadTerminated(BaseThread sender) {
                if (_ThreadManager.isEmpty()) {
                    showFinishMessage();
                    actionRestartUpdate.setEnabled(true);
                }
                updateHandler = null;
            }
        });

        updateHandler.start();
    }

    @Override
    public void dispose() {
        if (_Preferences.isAutoUpdateOn()) {
            _Utils.startUpdater();
        }
        super.dispose();
    }

    /**
     * Shows message box about job done.
     */
    private void showFinishMessage() {
        if (errorCount == 0) {
            MessageBox.showInfoMsg(Instance, null,
                    "Update finished. Please restart application to take effect.");
        } else {
            MessageBox.showWarningMsg(Instance, null,
                    "Update finished but there were errors. Please update again.");
        }
    }

    /**
     * Creates new {@link BinImageFileDecoder}
     * @param file the file points to a {@link BinImageFileDecoder}
     * @param itemUpdate an {@link ItemUpdate}, will be used to notify user about decoding progress.
     */
    private void createBinImageFileDecoder(final File file, final ItemUpdate itemUpdate) {
        int row = 0;
        for (row = 0; row < TableItemUpdatesModel.getRowCount(); row++) {
            if (itemUpdate.equals(TableItemUpdatesModel.getValueAt(row, ColItemUpdateIdx))) {
                break;
            }
        }
        final int Row = row;

        final BinImageFileDecoder decoder = new BinImageFileDecoder(file, FileTypes.BinaryImageSignature);
        decoder.addThreadListener(new ThreadAdapter() {

            private boolean hasErrors = false;

            @Override
            public synchronized void threadStarted(BaseThread sender) {
                TableItemUpdatesModel.setValueAt("Decoding file...", Row, ColStatusIdx);
                TableItemUpdatesModel.fireTableDataChanged();
            }

            @Override
            public synchronized void threadSignaled(BaseThread sender, Object message, Object obj) {
                switch ((Integer) message) {
                    case BinImageFileDecoder.KeyDecodingProgress: {
                        if (decoder.getBinImageFileSize() > 0) {
                            TableItemUpdatesModel.setValueAt(
                                    String.format("Decoding file - %s...",
                                    Converter.percentToStr(100.0 * (Long) obj / decoder.getBinImageFileSize())),
                                    Row, ColStatusIdx);
                            TableItemUpdatesModel.fireTableDataChanged();
                        }
                        break;
                    }//KeyDecodingProgress
                }
            }

            @Override
            public synchronized void threadRaised(BaseThread sender, Exception ex, Object obj) {
                TableItemUpdatesModel.setValueAt(String.format("Exception: %s", ex), Row, ColStatusIdx);
                TableItemUpdatesModel.fireTableDataChanged();
                errorCount++;
                hasErrors = true;
            }

            @Override
            public synchronized void threadTerminated(BaseThread sender) {
                if (!hasErrors) {
                    /*
                     * check for launcher or launcher-loader update,
                     * then store ItemUpdate to file
                     */
                    try {
                        if (Debug) {
                            System.out.printf("BinDecoder: Check for update of launcher...\n");
                        }
                        checkForUpdatesOfLaunchers(file, itemUpdate);
                        storeNewItemUpdate(itemUpdate);
                        TableItemUpdatesModel.setValueAt("Done", Row, ColStatusIdx);
                    } catch (Exception ex) {
                        errorCount++;
                        TableItemUpdatesModel.setValueAt(String.format("Exception: %s", ex), Row, ColStatusIdx);
                    }

                    TableItemUpdatesModel.fireTableDataChanged();
                }
            }
        });

        _ThreadManager.add(decoder);
        decoder.start();
    }

    /**
     * Checks if {@code itemUpdate} is one update of launchers. If it is, do update
     * it here. Because launcher, or launcher-loader can not update itself  :-)
     * @param file new file downloaded
     * @param itemUpdate {@link ItemUpdate}
     * @throws IOException means as its name
     */
    private void checkForUpdatesOfLaunchers(File file, final ItemUpdate itemUpdate) throws IOException {
        //if filepath is root-path
        if (itemUpdate.getFilepath().matches("[/\\\\]*")) {
            if (itemUpdate.getFilename().matches("launcher(-loader)?\\.jar")) {
                File currentFile = new File(String.format(
                        "%s/%s", _Utils.getStartupDir().getAbsolutePath(), itemUpdate.getFilename()));
                currentFile.delete();
                try {
                    Thread.sleep(999);
                } catch (InterruptedException ie) {
                }
                if (!file.renameTo(currentFile)) {
                    throw new IOException(String.format("Can not rename \"/%s\" to \"/%s\"",
                            file.getName(), currentFile.getName()));
                }
            }
        }
    }

    /**
     * Stores new {@link ItemUpdate} into update-info file.
     * @param newItemUpdate
     * @throws FileNotFoundException
     * @throws IOException
     * @throws BadFormatException 
     */
    private void storeNewItemUpdate(ItemUpdate newItemUpdate) throws FileNotFoundException,
            IOException, BadFormatException {
        InfoFile infoFile = new InfoFile(_Utils.getUpdateFile());
        boolean found = false;
        for (ItemUpdate item : infoFile) {
            if (item.getFilepath().equals(newItemUpdate.getFilepath())
                    && item.getFilename().equals(newItemUpdate.getFilename())) {
                found = true;

                infoFile.remove(item);
                if (!newItemUpdate.isFileToBeDeleted()) {
                    infoFile.add(newItemUpdate);
                }
                break;
            }
        }//for

        if (!found && !newItemUpdate.isFileToBeDeleted()) {
            infoFile.add(newItemUpdate);
        }

        infoFile.store(_Utils.getUpdateFile());
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
        java.awt.GridBagConstraints gridBagConstraints;

        jScrollPane1 = new javax.swing.JScrollPane();
        tableItemUpdates = new javax.swing.JTable();
        buttonRestart = new javax.swing.JButton();
        buttonClose = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        getContentPane().setLayout(new java.awt.GridBagLayout());

        tableItemUpdates.setModel(new javax.swing.table.DefaultTableModel(
            new Object [][] {

            },
            new String [] {

            }
        ));
        tableItemUpdates.getTableHeader().setReorderingAllowed(false);
        jScrollPane1.setViewportView(tableItemUpdates);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        gridBagConstraints.insets = new java.awt.Insets(8, 8, 8, 8);
        getContentPane().add(jScrollPane1, gridBagConstraints);

        buttonRestart.setText("jButton1");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LAST_LINE_START;
        gridBagConstraints.insets = new java.awt.Insets(8, 8, 8, 8);
        getContentPane().add(buttonRestart, gridBagConstraints);

        buttonClose.setText("jButton2");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LAST_LINE_END;
        gridBagConstraints.insets = new java.awt.Insets(8, 8, 8, 8);
        getContentPane().add(buttonClose, gridBagConstraints);

        pack();
    }// </editor-fold>//GEN-END:initComponents
//  /**
//   * @param args the command line arguments
//   */
//  public static void main(String args[]) {
//    /* Set the Nimbus look and feel */
//    //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
//        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
//     * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
//     */
//    try {
//      for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
//        if ("Nimbus".equals(info.getName())) {
//          javax.swing.UIManager.setLookAndFeel(info.getClassName());
//          break;
//        }
//      }
//    } catch (ClassNotFoundException ex) {
//      java.util.logging.Logger.getLogger(DialogUpdater.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
//    } catch (InstantiationException ex) {
//      java.util.logging.Logger.getLogger(DialogUpdater.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
//    } catch (IllegalAccessException ex) {
//      java.util.logging.Logger.getLogger(DialogUpdater.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
//    } catch (javax.swing.UnsupportedLookAndFeelException ex) {
//      java.util.logging.Logger.getLogger(DialogUpdater.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
//    }
//    //</editor-fold>
//
//    /* Create and display the dialog */
//    java.awt.EventQueue.invokeLater(new Runnable() {
//
//      public void run() {
//        DialogUpdater dialog = new DialogUpdater(new javax.swing.JFrame(), true);
//        dialog.addWindowListener(new java.awt.event.WindowAdapter() {
//
//          @Override
//          public void windowClosing(java.awt.event.WindowEvent e) {
//            System.exit(0);
//          }
//        });
//        dialog.setVisible(true);
//      }
//    });
//  }
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton buttonClose;
    private javax.swing.JButton buttonRestart;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JTable tableItemUpdates;
    // End of variables declaration//GEN-END:variables

    /*----------------------------------------------------------------------------
     * ActionRestartUpdate
     */
    private ActionRestartUpdate actionRestartUpdate;

    private class ActionRestartUpdate extends AbstractAction {

        public ActionRestartUpdate() {
            super("Restart update...");
        }

        public void actionPerformed(ActionEvent e) {
            synchronized (_ThreadManager) {
                if (updateHandler != null || !_ThreadManager.isEmpty()) {
                    if (!MessageBox.confirmYesNo(Instance, null, "Are you sure you want to restart update?", 1)) {
                        return;
                    }
                }

                if (updateHandler != null) {
                    updateHandler.interrupt();
                }
                _ThreadManager.interruptAll();
                updateHandler = null;

                for (int row = 0; row < TableItemUpdatesModel.getRowCount(); row++) {
                    TableItemUpdatesModel.setValueAt(null, row, ColStatusIdx);
                }
                TableItemUpdatesModel.fireTableDataChanged();

                start();
            }
        }
    }

    /*----------------------------------------------------------------------------
     * ActionClose
     */
    private ActionClose actionClose;

    private class ActionClose extends AbstractAction {

        public ActionClose() {
            super("Close");
        }

        public void actionPerformed(ActionEvent e) {
            Instance.dispatchEvent(new WindowEvent(Instance, WindowEvent.WINDOW_CLOSING));
        }
    }
}
